###############################################################################
#                                                                             #
#    This file is part of the "ssbot" Python module.                          #
#    It is distributed under the MIT License. See LICENSE.txt for details.    #
#                                                                             #
###############################################################################

import json
import os
import Queue
import threading
import time
import xmlrpclib

import ssdb
import ssutils


class Builder(object):
    def __init__(self, name, scm_list, step_list, remote_url=None):
        """ Constructor.

        Arguments:
            name -- The name of the builder.
            scm_list -- The list of SCMs the builder has to deal with.
            step_list -- The list of steps the builder has to run for each
                         build.
        """
        ssutils.validate_name(name)
        for repo_name in scm_list:
            ssutils.validate_name(name)
        self.name = name
        self.scm_list = scm_list
        self.step_list = step_list
        self.in_queue = None
        self.remote_url = remote_url
        self.use_remote_build_slave = self.remote_url is not None

        # These are bunch of convenience instance attributes.
        self.ws_path = os.path.abspath(os.path.join('.', name))
        self.archive_path = os.path.join(self.ws_path, 'builds')

        if not os.path.exists(self.ws_path):
            os.mkdir(self.ws_path)

        if not os.path.exists(self.archive_path):
            os.mkdir(self.archive_path)

        if self.use_remote_build_slave:
            self.remote_proxy = xmlrpclib.ServerProxy(self.remote_url,
                                                      allow_none=True)
            self.remote_ws_path = self.name
            self.remote_proxy.mkdir(self.remote_ws_path)

        self.db = ssdb.BuilderDB(self.name)
        self.current_build = {}  # The current build.

    def get_name(self):
        return self.name

    def has_remote_build_slave(self):
        return self.use_remote_build_slave

    def get_remote_proxy(self):
        if self.use_remote_build_slave:
            return self.remote_proxy
        else:
            return None

    def get_ws_path(self):
        if self.use_remote_build_slave:
            return self.remote_ws_path
        else:
            return self.ws_path

    def transfer_to_build_slave(self, content, file_name):
        assert(self.use_remote_build_slave)
        return self.remote_proxy.write_to_file(content, file_name)

    def connect(self, in_queue):
        """ Connects the builder to an input queue of build objects.

        This method should only be called by a scheduler.

        Arguments:
            in_queue -- The input queue the builder has to listen to.
        """
        self.in_queue = in_queue
        return self.db

    def __call__(self):
        if not self.in_queue:
            return
        while True:
            try:
                build_object = self.in_queue.get(block=False)
                self.start_build(build_object)
                retval = self.build_it(build_object)
                self.finish_build(retval)
            except Queue.Empty:
                pass
            time.sleep(ssutils.BUILD_INTERVAL)

    def build_it(self, build_object):
        """Should be overidden by a concrete builder class.

        Arguments:
            build_object - The build object which triggered the build
        """
        raise NotImplementedError()

    def make_build_directory(self, build_number):
        """ Creates a directory with name BUILD_NUMBER in the archive path.
        """
        build_number_path = os.path.join(
            self.archive_path, '%d' % build_number)
        if not os.path.exists(build_number_path):
            os.mkdir(build_number_path)
        return build_number_path

    def save_step_outputs(self, step_result, build_number):
        build_number_path = self.make_build_directory(build_number)
        step_path = os.path.join(build_number_path, step_result['name'])
        if not os.path.exists(step_path):
            os.mkdir(step_path)
        out_file = os.path.join(step_path, 'stdout.txt')
        err_file = os.path.join(step_path, 'stderr.txt')
        with open(out_file, 'w') as f:
            f.write('Command "%s" in "%s".\n' %
                    (' '.join(step_result['cmd']), step_result['cwd']))
            f.write(step_result['stdout'])
        with open(err_file, 'w') as f:
            f.write('Command "%s" in "%s".\n' %
                    (' '.join(step_result['cmd']), step_result['cwd']))
            f.write(step_result['stderr'])
        if 'data' in step_result:
            data_file = os.path.join(step_path, 'data.txt')
            with open(data_file, 'w') as f:
                f.write(step_result['data'])

    def run_step(self, step, build_number):
        step_stats = { 'name': step.name, 'has_data': False }
        step_stats['status'] = ssutils.STATUS_RUNNING
        step_stats['start_time'] = time.strftime('%Y-%m-%d | %H:%M:%S')
        self.start_step(step_stats)
        try:
            if self.remote_url:
                step_result = self.remote_proxy.run_step(step,
                                                         self.remote_ws_path)
            else:
                step_result = step.run(self.ws_path)
        except Exception as e:
            step_result = {
                'name': step.name,
                'cmd': step.cmd,
                'retcode': 1,
                'stdout': ssutils.RESULT_EXCEPTION,
                'stderr': str(e),
                'cwd': step.cwd 
            }
            step_stats['result'] = ssutils.RESULT_EXCEPTION
        step_stats['end_time'] = time.strftime('%Y-%m-%d | %H:%M:%S')
        step_stats['status'] = ssutils.STATUS_COMPLETED
        step_stats['retcode'] = step_result['retcode']
        if 'data' in step_result:
            step_stats['has_data'] = True
        if step_result['retcode'] == 0:
            step_stats['result'] = ssutils.RESULT_PASS
        else:
            if not 'result' in step_stats:
                step_stats['result'] = ssutils.RESULT_FAIL
        self.finish_step(step_stats)
        self.save_step_outputs(step_result, build_number)
        return step_result
    
    def run_steps(self, step_list, build_number):
        completed_steps = []
        retcode = 0
        for s in step_list:
            skip = False
            if s.deps:
                for d in s.deps:
                    if not d.name in completed_steps:
                        skip = True
                        break
            if skip:
                continue
            step_result = self.run_step(s, build_number)
            retcode += abs(step_result['retcode'])
            if step_result['retcode'] == 0:
                completed_steps.append(s.name)
        return retcode

    def start_step(self, step_stats):
        self.db.add_step(self.current_build['build_number'],
                         step_stats['name'],
                         step_stats['start_time'])
        self.current_build['current_step'] = step_stats

    def finish_step(self, step_stats):
        self.db.finish_step(self.current_build['build_number'],
                            step_stats['name'],
                            step_stats['result'],
                            step_stats['end_time'])
        self.current_build['completed_steps'].append(step_stats)
        self.current_build['current_step'] = None

    def start_build(self, build_object):
        start_time = time.strftime('%Y-%m-%d | %H:%M:%S')
        self.db.start_build(build_object['build_number'], start_time)
        build_stats = {
            'build_number': build_object['build_number'],
            'reason': build_object['reason'],
            'current_step': None,
            'completed_steps': [],
            'start_time': start_time,
            'status': ssutils.STATUS_RUNNING,
            'end_time': None
        }
        self.current_build = build_stats

    def finish_build(self, retcode):
        endtime = time.strftime('%Y-%m-%d | %H:%M:%S')
        result = ssutils.RESULT_FAIL if retcode else ssutils.RESULT_PASS
        self.db.finish_build(
            self.current_build['build_number'], result, endtime)
        self.current_build = {}


class LocalBuilder(Builder):
    def save_blame_list(self, blame_batch, build_number):
        build_number_path = self.make_build_directory(build_number)
        blame_batch_path = os.path.join(build_number_path, 'blame_list.txt')
        if not os.path.exists(blame_batch_path):
            with open(blame_batch_path, 'w') as f:
                for repo_name in blame_batch:
                    f.write('%s:\n' % repo_name)
                    for blame in blame_batch[repo_name][0]:
                        f.write('\t - %s\n' % blame)

    def build_it(self, build_number):
        blame_batch = self.pop_next_pending_build()
        self.save_blame_list(blame_batch, build_number)
        if not blame_batch:
            return 0
        for scm in self.scm_list:
            sync_steps = scm.sync_steps(blame_batch[scm.repo_name][1])
            sync_retcode = self.run_steps(sync_steps, build_number)
            if sync_retcode:
                return sync_retcode
        retcode = self.run_steps(self.step_list, build_number)
        return (retcode, blame_batch)


class TryBuilder(Builder):
    def save_patches(self, build_object):
        build_number = build_object['build_number']
        build_number_path = self.make_build_directory(build_number)
        patch_file_map = {}
        for scm in build_object['patch_map']:
            patch_file_name = os.path.join(build_number_path,
                                           '%s-patch.txt' % scm)
            with open(patch_file_name, 'w') as f:
                f.write(build_object['patch_map'][scm])
            if self.has_remote_build_slave():
                patch_file_name_base = '%s-patch.txt' % scm
                patch_file_name = self.transfer_to_build_slave(
                    build_object['patch_map'][scm], patch_file_name_base)
            patch_file_map[scm] = patch_file_name
        return patch_file_map

    def build_it(self, build_object):
        retcode = 0
        if not build_object['patch_map']:
            return 1
        patch_file_map = self.save_patches(build_object)
        for scm in self.scm_list:
            sync_steps = scm.sync_steps(self.get_ws_path(),
                                        remote=self.get_remote_proxy())
            sync_retcode = self.run_steps(sync_steps,
                                          build_object['build_number'])
            if sync_retcode:
                retcode += sync_retcode
                break
            if scm.repo_name in build_object['patch_map']:
                patch_step = scm.patch_step(patch_file_map[scm.repo_name])
                patch_retcode = self.run_steps([patch_step],
                                               build_object['build_number'])
                if patch_retcode:
                    retcode += patch_retcode
                    break
        if retcode == 0:
            retcode += self.run_steps(self.step_list,
                                      build_object['build_number'])
        return retcode
